/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package edu.upc.sma.agents;

import com.springrts.ai.AICommand;
import com.springrts.ai.AICommandWrapper;
import com.springrts.ai.AIFloat3;
import com.springrts.ai.command.AttackUnitAICommand;
import com.springrts.ai.command.BuildUnitAICommand;
import com.springrts.ai.command.MoveUnitAICommand;
import com.springrts.ai.command.SendTextMessageAICommand;
import com.springrts.ai.oo.Economy;
import com.springrts.ai.oo.OOAICallback;
import com.springrts.ai.oo.Resource;
import com.springrts.ai.oo.Unit;
import com.springrts.ai.oo.UnitDef;
import com.springrts.ai.oo.WeaponDef;
import edu.upc.sma.visual.GUIboss;
import edu.upc.sma.visual.JASONboss;
import edu.upc.sma.visual.SpringBoss;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

/**
 *
 * @author xavi
 */
public class MySpringAgent extends com.springrts.ai.oo.AbstractOOAI {
    /**
     * Object through which the agent communicates with the engine
     */
    private OOAICallback callback;

    /**
     * Reference to agent's commander.  The agent gets this reference when
     * <code>unitFinished</code> method is called.
     */
    private Unit commander;

    /**
     * Reference to team of the agent.  The agent gets this reference when
     * <code>init</code> method is called.
     */
    private int teamId;

    /**
     * Resources
     */
    List<Resource> resources;
    private final int METAL = 0;
    private final int ENERGY = 1;

    /**
     * Map information
     */
    private MapInfo mapInfo;

    /**
     * Reference to who will take the following action. The agent gets this
     * reference when <code>move</code>, <code>construct</code>,
     * <code>attack</code> or <code>sendMessage</code> method is called.
     */
    private Unit unitOrigin;

    /**
     * Reference to unit that will be the target of the following action of the
     * agent. The agent gets this reference when <code>attack</code> or
     * <code>sendMessage</code> method is called.
     */
    private Unit unitTarget;

    /**
     * Reference to unit definition that agent will construct.  The agent gets
     * this reference when <code>construct</code> method is called.
     */
    private UnitDef construction;

    /**
     * The default orientation for all buildings.
     */
    private int  defaultFacing;

    /**
     * Message to be sent in the following <code>sendMessage</code> action
     */
    private String message;

    /**
     * Zone where will be sent the following message.
     */
    private int zone;

    /**
     * Point where will be done the following action
     */
    private AIFloat3 point;

    /**
     * Last commander position
     */
    private AIFloat3 lastPosition;

    /**
     * Determines what is the following action that the agent will try to fulfil
     * </p>
     * Can take the values:
     * <ul>
     * </li> ERROR    (-1): Error
     * </li> NOTHING   (0): Nothing to do
     * </li> MOVE      (1): Move
     * </li> CONSTRUCT (2): Construct
     * </li> ATTACK    (3): Attack
     * </li> MESSAGE   (4): Send a message
     * </ul>
     *
     */
    private int behaviour;

    /**
     * Constructions handled and not finished.
     * </p>
     * Allows tracking the command during the execution for identify a possible
     * result event.
     */
    private ArrayList<String> currentConstructions;
    /**
     * Values that can take the {@link behaviour} atribure
     */
    public final int ERROR=-1, NOTHING=0, MOVE=1, CONSTRUCT=2, ATTACK=3,
            MESSAGE=4;

    /**
     * Unit names
     */
    public final String COMMANDER="armcom", SOLAR_PLANT="armsolar",
            VEHICLE_PLANT="armvp", MISILE_VEHICLE="armsam";

    /**
     * Useful for distribute construction works
     */
    Unit vehicle_plant;

    /**
     * Agent graphic interface
     */
    private SpringBoss multiplexer;

    private final int JASON=1, GUI=2;

//    private PrintStream outfile;


    /**
     * <code>init</code> method is called at the start of every game (right at
     * the end of the countdown)
     *
     * @param teamId
     * @param callback Object through which the agent communicates with the
     *                 engine
     * @return
     */
    @Override
    public int init(int teamId, OOAICallback callback) {
        this.teamId = teamId;
        this.callback = callback;
        resources = callback.getResources();
        currentConstructions = new ArrayList<String>();
        behaviour = NOTHING;
        vehicle_plant = null;

        // Read map information:
        mapInfo = new MapInfo(callback.getMap(), resources.get(METAL));
        defaultFacing = 0;
        //callback.getMap().getHeightMap();

        // Read mod information
        //callback.getMod().get...

        int op=GUI;

        switch(op){
            case GUI:   multiplexer = new GUIboss(this);
                        ((GUIboss)multiplexer).setVisible(true);
                break;
            case JASON: multiplexer = new JASONboss(this);
                break;
            default:throw new UnsupportedOperationException("Not supported yet.");
                //break;
        }

        // ---------------
//        FileOutputStream Output;
//        try
//        {
//            Output = new FileOutputStream("/home/xavi/Escritorio/out.txt");
//            outfile = new PrintStream(Output);
//        }
//        catch  (Exception e)
//        {
//            System.out.println("Could not load file!");
//        }
        // ---------------

        return 0;
    }

    // +++++++++++++++++++++++++     EVENTS     +++++++++++++++++++++++++

    /**
     * UnitFinished event is triggered whenever a unit has been built.
     * <p/>
     * At the start of the game, the engine will send a 'UnitFinished' event for
     * every starting unit. When playing BA, this means you will get a
     * 'UnitFinished' event for the commander.
     *
     * @param unit Reference to agent's commander
     * @return
     */
    @Override public int unitFinished(Unit unit) {
        // To get a reference to the commander, is necessary to check if the
        // unit is the command
        if(commander==null){
            if (unit.getDef().getName().equals(COMMANDER)){
                this.commander = unit;
                lastPosition = new AIFloat3(-1,-1,-1);
                multiplexer.refreshTeamUnits(callback.getTeamUnits());
                multiplexer.gameStarted();
            }
        }else{
            multiplexer.addEvent("[unitFinished] "+unit.getDef().getHumanName()+
                    " (team: "+unit.getTeam()+")");
            if(unit.getDef().getName().equals(VEHICLE_PLANT)){
                vehicle_plant = unit;
            }
            String name = unit.getDef().getName();
            if(!currentConstructions.remove(name)){
                multiplexer.addEvent("<unitFinished not constructed by this team> "+
                        unit.getDef().getHumanName());
            }
            else{
            // Is just erased the last object with this name, therfore the order
            // is finished
                if(!currentConstructions.contains(name)){
                    try{
                        int id = Integer.parseInt(name);
                        multiplexer.orderUnitsConstructed(id);
                    }
                    catch(NumberFormatException e){
                        multiplexer.buildingConstructed(name);
                    }
                }
            }
        }
        return 0;
    }

    /**
     * Is called every time a unit is created, and tells you the unit that was
     * created and the unit that built it (can be null)
     *<p/>
     * Is called when starts the unit building.
     *
     * @param unit
     * @param commander
     * @return
     */
    @Override
    public int unitCreated(Unit unit, Unit commander){
        multiplexer.addEvent("[unitCreated] "+unit.getDef().getHumanName()+
                " (team: "+unit.getTeam()+")");
        multiplexer.refreshTeamUnits(callback.getTeamUnits());
        return 0;
    }

    @Override
   public int unitCaptured(Unit unit, int oldTeamId, int newTeamId){
        multiplexer.addEvent("[unitCaptured] "+unit.getUnitId()+
                " (from team "+oldTeamId+" to"+newTeamId+")");
        return 0;
    }

    @Override
    public int unitDamaged(Unit unit, Unit attacker, float damage, AIFloat3 dir, WeaponDef weaponDef, boolean paralyzer){
        multiplexer.addEvent("[unitDamaged] "+unit.getUnitId()+
                " (attacker: "+attacker.getUnitId()+", damage: "+damage+
                ", dir: "+dir.toString()+", paralyzer: "+paralyzer+")");
        return 0;
    }

    @Override
    public int unitDestroyed(Unit unit, Unit attacker){
        multiplexer.addEvent("[unitDestroyed] "+unit.getUnitId()+
                " (attacker: "+attacker.getUnitId()+")");
        multiplexer.refreshTeamUnits(callback.getTeamUnits());
        return 0;
    }

    @Override
    public int unitMoveFailed(Unit unit){
        multiplexer.addEvent("[unitMoveFailed] "+unit.getUnitId());
        return 0;
    }

    @Override
    public int unitIdle(Unit unit){
        multiplexer.addEvent("[unitIdle] "+unit.getUnitId());
        return 0;
    }

    @Override
    public int unitGiven(Unit unit, int oldTeamId, int newTeamId){
        multiplexer.addEvent("[unitGiven] "+unit.getUnitId()+
                " (from team "+oldTeamId+" to"+newTeamId+")");
        return 0;
    }

    @Override
    public int enemyEnterRadar(Unit enemy){
        AIFloat3 p = enemy.getPos();
        String str = "[enemyEnterRadar] "+enemy.getUnitId()+ "("+p.x+","+p.z+
                ") (team: "+enemy.getTeam()+")";
        multiplexer.addEvent(str);
        return 0;
    }

    @Override
    public int enemyLeaveRadar(Unit enemy){
        String str = "[enemyLeaveRadar] "+enemy.getUnitId()+
                " (team: "+enemy.getTeam()+")";
        multiplexer.addEvent(str);
        return 0;
    }

    @Override
    public int enemyEnterLOS(Unit enemy){
        if(enemy.getDef().getName().equals(COMMANDER)){
            AIFloat3 p = enemy.getPos();
            Vector<Float> v = new Vector<Float>();
            v.add(p.x);
            v.add(p.y);
            v.add(p.z);
            multiplexer.enemyCommanderEnterLOS(v);
        }
        else{
            String str = "[enemyEnterLOS] "+enemy.getUnitId()+
                    " (team: "+enemy.getTeam()+")";
            multiplexer.addEvent(str);
        }
        return 0;
    }

    @Override
    public int enemyLeaveLOS(Unit enemy){
        String str = "[enemyLeaveLOS] "+enemy.getUnitId()+
                " (team: "+enemy.getTeam()+")";
        multiplexer.addEvent(str);
        return 0;
    }

    @Override
    public int enemyDamaged(Unit enemy, Unit attacker, float damage, AIFloat3 dir, WeaponDef weaponDef, boolean paralyzer){
        multiplexer.addEvent("[enemyDamaged] "+enemy.getUnitId()+
                " (attacker: "+attacker.getUnitId()+", damage: "+damage+
                ", dir: "+dir.toString()+", paralyzer: "+paralyzer+")");
        return 0;
    }

    @Override
    public int enemyDestroyed(Unit enemy, Unit attacker){
        multiplexer.addEvent("[enemyDestroyed] "+enemy.getUnitId()+
                " (attacker: "+attacker.getUnitId()+")");
        return 0;
    }

    @Override
    public int weaponFired(Unit unit, WeaponDef weaponDef){
        multiplexer.addEvent("[weaponFired] "+unit.getUnitId()+
                " (weaponDef: "+weaponDef.getName()+")");
        return 0;
    }

    @Override
    public int seismicPing(AIFloat3 pos, float strength){
        multiplexer.addEvent("[seismicPing] pos:"+pos.toString()+
                ", strength: "+strength);
        return 0;
    }

    @Override
    public int commandFinished(Unit unit, int commandId, int commandTopicId){
        String str_command = "";
        multiplexer.addEvent("[commandFinished] unit: "+unit.getUnitId()+
                ", commandId: "+commandId+", commandTopicId: "+commandTopicId);
        return 0;
    }

    @Override
    public int message(int player, String message){
        multiplexer.addEvent("[message] player: "+player+" message: "+message);
        return 0;
    }

    // ++++++++++++++++++++++   EVERY FRAME OF THE GAME   ++++++++++++++++++++++

    /**
     * This method is called at every frame of the game
     *
     * @param frame actual frame of the game
     * @return
     */
    @Override public int update(int frame) {
//        outfile.println("[update] frame: "+frame);

        // +++++++++++++++++++   REFRESH VALUES   +++++++++++++++++++

        multiplexer.refreshEnemyLOSandRadar(callback.getEnemyUnits(),
                                        callback.getEnemyUnitsInRadarAndLos());

        if(frame%5==0){
            AIFloat3 c = commander.getPos();
            if(lastPosition.x!=c.x || lastPosition.y!=c.y || lastPosition.z!=c.z){
                try{
                    multiplexer.refreshMines(this.distanceToMetalMapSpots());
                    lastPosition=c;
                }catch(Exception e){
//                    outfile.println("message: "+e.getMessage()+"; class: "+e.getClass()+"; Stack: ");
//                    StackTraceElement[] l = e.getStackTrace();
//                    for(int i=0; i<l.length; i++){
//                        outfile.println(l[i]);
//                    }
                }
            }

            Economy e = callback.getEconomy();
            Resource r = resources.get(METAL);
            multiplexer.refreshMetalEconomy(r.getName(),e.getCurrent(r),
                    e.getIncome(r), e.getStorage(r), e.getUsage(r));
            r = resources.get(ENERGY);
            multiplexer.refreshEnergyEconomy(r.getName(),e.getCurrent(r),
                    e.getIncome(r), e.getStorage(r), e.getUsage(r));

        }

        // ++++++++++++++++++   CHANGE BEHAVIOUR   ++++++++++++++++++

        switch (behaviour){
            case NOTHING:
                break;
            case MOVE:      this.move();
                break;
            case CONSTRUCT: this.construct();
                break;
            case ATTACK:    this.attack();
                break;
            case MESSAGE:   this.sendMessage();
                break;
             default:
                break;
        }

        return 0;
    }

    // +++++++++++++++++++++++++   PRIVATE METHODS   +++++++++++++++++++++++++

    private void move(){
        int groupID = -1;
        int timeOut = 10000;
        AICommand command = new MoveUnitAICommand(unitOrigin, groupID,
                             new ArrayList<AICommand.Option>(), timeOut, point);
        this.handleCommand(command);
    }

    private void construct(){
        int groupID = -1;
        int timeOut = 10000;
        AICommand command = new BuildUnitAICommand(unitOrigin, groupID,
                    new ArrayList<AICommand.Option>(), timeOut, construction,
                    point, defaultFacing);
//            BuildUnitAICommand(
//                    the unit we want to give to order to,
//                    the group we want to give the order to = -1, --> In this example we only have a single unit
//                    a list addional options. See AICommand.Option for details,
//                    the number of frames after which the command times out,
//                    the UnitDef of the unit we want to build,
//                    the position to build the unit at,
//                    the direction the new unit should face)
        this.handleCommand(command);
    }

    private void attack(){
        int groupID = -1;
        int timeOut = 10000;
        AICommand command = new AttackUnitAICommand(unitOrigin, groupID,
                          new ArrayList<AICommand.Option>(), timeOut, unitTarget);
        this.handleCommand(command);
    }

    private void sendMessage(){
        AICommand command = new SendTextMessageAICommand(message, zone);
        this.handleCommand(command);
    }

    /**
     * Give the command to the engine
     */
    private void handleCommand(AICommand command){
        this.callback.getEngine().handleCommand(
                            AICommandWrapper.COMMAND_TO_ID_ENGINE, -1, command);
//            callback.getEngine().handleCommand(
//                id of the recipient of the command,
//                commandID = -1,  --> this is a feature which is not yet supported
//                the command itself)      by spring (asynchronous command tracking)
        multiplexer.addEvent("<handleCommand> commandTopicId: "+command.getTopic());
        behaviour = this.NOTHING;
    }

    private List<AIFloat3> distanceToMetalMapSpots() throws Exception{
        List<AIFloat3> minesList = new ArrayList<AIFloat3>();
        AIFloat3 cp = commander.getPos();
        for(int i=0; i<mapInfo.getNmines(); i++){
            AIFloat3 p = mapInfo.getMetalMapSpotsPosition(i);
            AIFloat3 d = new AIFloat3((p.x-cp.x),(p.y-cp.y),(p.z-cp.z));
            // Sort by proximity
            boolean added =false;
            int j=0;
            while(j<minesList.size() && !added){
                if(lessDistance(d,minesList.get(j))){
                    minesList.add(j, d);
                    added=true;
                }else{
                    j++;
                }
            }
            if(!added){
                minesList.add(d);
            }
        }
        return minesList;
    }
    /**
     * Returns true if the distance from origin (0,0,0) and p1 is less than the
     * distance from origin to p2.
     *
     * @param p1
     * @param p2
     * @return
     */
    private boolean lessDistance(AIFloat3 p1, AIFloat3 p2){
        if(Math.sqrt(Math.pow(p1.x, 2)+Math.pow(p1.y, 2)+Math.pow(p1.z, 2)) <
                Math.sqrt(Math.pow(p2.x, 2)+Math.pow(p2.y, 2)+Math.pow(p2.z, 2))){
            return true;
        }
        else{
            return false;
        }
    }

    // +++++++++++++++++++++++++     API     +++++++++++++++++++++++++

    /**
     * vector[0] is width and vector[1] is height
     * @return
     */
    public Vector<Integer> getMapMeasures(){
        Vector<Integer> v = new Vector<Integer>();
        v.add(mapInfo.getWidth());
        v.add(mapInfo.getHeight());
        return v;
    }

    /**
     * vector[0] is x, vector[1] is y and vector[2] z
     * @return
     */
    public Vector<Float> getCommanderPosition(){
        AIFloat3 p = commander.getPos();
        Vector<Float> v = new Vector<Float>();
        v.add(p.x);
        v.add(p.y);
        v.add(p.z);
        return v;
    }
    
    public float getMetalAvailable(){
        Economy e = callback.getEconomy();
        return e.getCurrent(resources.get(METAL));
    }
    
    public float getEnergyAvailable(){
        Economy e = callback.getEconomy();
        return e.getCurrent(resources.get(ENERGY));
    }

    public float getMetalCost(String unit){
        return this.getUnitDef(unit).getCost(resources.get(METAL));
    }

    public float getEnergyCost(String unit){
        return this.getUnitDef(unit).getCost(resources.get(ENERGY));
    }

    public String getDependence(String unit){
        if(MISILE_VEHICLE.equals(unit)){
            return VEHICLE_PLANT;
        }
        else{
            return null;
        }
    }

    public void move(Unit unit, float x, float z){
        AIFloat3 commanderPos = this.commander.getPos();
        float y = mapInfo.getElevationAt(x, z);
        point = new AIFloat3(commanderPos.x+x,commanderPos.y+y,
                commanderPos.z+z);//y: down x: right
        unitOrigin=unit;
        behaviour = this.MOVE;
    }

    public void construct(Unit constructor, String construction, float x, float z){
        AIFloat3 commanderPos = this.commander.getPos();
        float y = mapInfo.getElevationAt(x, z);
        point = new AIFloat3(commanderPos.x+x,commanderPos.y+y,
                commanderPos.z+z);//y: down x: right
        this.construction=this.getUnitDef(construction);
        if(mapInfo.isPossibleToBuildAt(this.construction, point, defaultFacing)){
            unitOrigin=constructor;
            currentConstructions.add(construction);
            behaviour = this.CONSTRUCT;
        }
        else{
            multiplexer.addEvent("<impossibleToConstruct> construction: "+
                    this.construction.getHumanName()+" position: ("+
                    point.x+", "+point.z+")");
            behaviour = this.NOTHING;
        }
        
    }

    /**
     *
     * @param construction building to construct
     * @return -1 if it is not possible to command the action because there are
     * another one in course
     * <p/>
     * 0 otherwise
     */
    public int defaultBuildingConstruct(String construction){
        if(behaviour != this.NOTHING) return -1;
        unitOrigin = commander;
        this.construction=this.getUnitDef(construction);
        point = mapInfo.findClosestBuildSite(this.construction,
                unitOrigin.getPos(), defaultFacing);
        currentConstructions.add(construction);
        behaviour = this.CONSTRUCT;
        return 0;
    }

    /**
     *
     * @param construction movile unit to construct
     * @param id identification number for the order of mobile units
     * @return -1 if it is not possible to command the action because there are
     * another one in course
     * <p/>
     * 1 if it is not possible to construct the mobile unit because the factory is not constructed yet
     * <p/>
     * 0 otherwise
     */
    public int defaultUnitConstruct(String construction, int id){
        if(behaviour != this.NOTHING) return -1;
        if(MISILE_VEHICLE.equals(construction) && vehicle_plant != null){
            unitOrigin = vehicle_plant;
            this.construction=this.getUnitDef(construction);
            point = mapInfo.findClosestBuildSite(this.construction,
                    unitOrigin.getPos(), defaultFacing);
            currentConstructions.add(""+id);
            behaviour = this.CONSTRUCT;
            return 0;
        }
        else{
            multiplexer.addEvent("<impossibleToConstruct> construction: "+
                    construction+"vehicle plant is not constructed yet");
            behaviour = this.NOTHING;
            return 1;
        }
    }

    public void attack(Unit attacker, Unit enemy){
        unitOrigin=attacker;
        unitTarget=enemy;
        behaviour=this.ATTACK;
    }

    public void sendMessage(Unit sender, Unit reciever){
        unitOrigin=sender;
        unitTarget=reciever;
        behaviour=this.MESSAGE;
    }

    public UnitDef getUnitDef(String unitName){
        List<UnitDef> unitDefs = this.callback.getUnitDefs();
        // callback.getUnitDefs()  will give you a list of all the available
        // unit types in the game that is currently being played

        for (UnitDef uDef : unitDefs){
            if (uDef.getName().equals(unitName))
            {
                return uDef;
            }
        }
        //outfile.println("[getUnitDef]  retorna null!!");
        return null;
    }
}
